package k8gbendpoint

/*
Copyright 2021-2025 The k8gb Contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
*/

import (
	"context"
	"fmt"

	"github.com/k8gb-io/k8gb/controllers/geotags"
	"github.com/k8gb-io/k8gb/controllers/resolver"
	"github.com/k8gb-io/k8gb/controllers/utils"

	k8gbv1beta1 "github.com/k8gb-io/k8gb/api/v1beta1"
	"github.com/rs/zerolog"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	externaldnsApi "sigs.k8s.io/external-dns/apis/v1alpha1"
	externaldns "sigs.k8s.io/external-dns/endpoint"
)

type UpdateRuntimeStatus func(gslb *k8gbv1beta1.Gslb, isPrimary bool, isHealthy k8gbv1beta1.HealthStatus, finalTargets []string)
type ApplicationDNSEndpoint struct {
	context             context.Context
	endpointType        dnsEndpointType
	client              client.Client
	config              *resolver.Config
	gslb                *k8gbv1beta1.Gslb
	logger              *zerolog.Logger
	updateRuntimeStatus UpdateRuntimeStatus
	queryService        utils.DNSQueryService
}

func NewApplicationDNSEndpoint(
	ctx context.Context,
	client client.Client,
	config *resolver.Config,
	gslb *k8gbv1beta1.Gslb,
	logger *zerolog.Logger,
	queryService utils.DNSQueryService,
	urs UpdateRuntimeStatus) *ApplicationDNSEndpoint {
	return &ApplicationDNSEndpoint{
		context:             ctx,
		client:              client,
		config:              config,
		endpointType:        applicationDNSEndpoint,
		gslb:                gslb,
		logger:              logger,
		queryService:        queryService,
		updateRuntimeStatus: urs,
	}
}

func (d *ApplicationDNSEndpoint) SaveDNSEndpoint(e *externaldnsApi.DNSEndpoint) error {
	return saveDNSEndpoint(d.context, d.client, d.gslb.Namespace, e, d.logger)
}

func (d *ApplicationDNSEndpoint) RemoveEndpoint() error {
	return removeEndpoint(d.context, d.client, client.ObjectKey{Namespace: d.gslb.Namespace, Name: d.gslb.Name}, d.logger)
}

func (d *ApplicationDNSEndpoint) GetDNSEndpoint() (*externaldnsApi.DNSEndpoint, error) {
	var gslbHosts []*externaldns.Endpoint
	var ttl = externaldns.TTL(d.gslb.Spec.Strategy.DNSTtlSeconds)

	localTargets := d.gslb.Status.LoadBalancer.ExposedIPs

	for host, health := range d.gslb.Status.ServiceHealth {
		var finalTargets = NewTargets()

		if !d.config.DelegationZones.ContainsZone(host) {
			return nil, fmt.Errorf("ingress host %s does not match delegated zone %v", host, d.config.DelegationZones.ListZones())
		}

		isPrimary := d.gslb.Spec.Strategy.PrimaryGeoTag == d.config.ClusterGeoTag
		isHealthy := health == k8gbv1beta1.Healthy

		if isHealthy {
			finalTargets.Append(d.config.ClusterGeoTag, localTargets)
			localTargetsHost := fmt.Sprintf("localtargets-%s", host)
			dnsRecord := &externaldns.Endpoint{
				RecordType: "A",
				Targets:    localTargets,
				RecordTTL:  ttl,
				DNSName:    localTargetsHost,
			}
			gslbHosts = append(gslbHosts, dnsRecord)
		}

		// Check if host is alive on external Gslb
		externalTargets := d.GetExternalTargets(host)
		externalTargets.Sort()

		if len(externalTargets) > 0 {
			switch d.gslb.Spec.Strategy.Type {
			case resolver.RoundRobinStrategy, resolver.GeoStrategy:
				finalTargets.AppendTargets(externalTargets)
			case resolver.FailoverStrategy:
				// If cluster is Primary
				if isPrimary {
					// If cluster is Primary and Healthy return only own targets
					// If cluster is Primary and Unhealthy return all external targets
					if !isHealthy {
						finalTargets = externalTargets
						d.logger.Info().
							Str("gslb", d.gslb.Name).
							Str("cluster", d.gslb.Spec.Strategy.PrimaryGeoTag).
							Strs("targets", finalTargets.GetIPs()).
							Str("workload", k8gbv1beta1.Unhealthy.String()).
							Msg("Executing failover strategy for primary cluster")
					}
				} else {
					// If cluster is Secondary and Primary external cluster is Healthy
					// then return Primary external targets
					// otherwise return all other targets
					if _, ok := externalTargets[d.gslb.Spec.Strategy.PrimaryGeoTag]; ok {
						finalTargets = NewTargets()
						finalTargets.Append(d.gslb.Spec.Strategy.PrimaryGeoTag, externalTargets[d.gslb.Spec.Strategy.PrimaryGeoTag].IPs)
					} else {
						finalTargets.AppendTargets(externalTargets)
					}
					d.logger.Info().
						Str("gslb", d.gslb.Name).
						Str("cluster", d.gslb.Spec.Strategy.PrimaryGeoTag).
						Strs("targets", finalTargets.GetIPs()).
						Str("workload", k8gbv1beta1.Healthy.String()).
						Msg("Executing failover strategy for secondary cluster")
				}
			}
		} else {
			d.logger.Info().
				Str("host", host).
				Msg("No external targets have been found for host")
		}

		d.updateRuntimeStatus(d.gslb, isPrimary, health, finalTargets.GetIPs())
		d.logger.Info().
			Str("gslb", d.gslb.Name).
			Strs("targets", finalTargets.GetIPs()).
			Msg("Final target list")

		if len(finalTargets) > 0 {
			dnsRecord := &externaldns.Endpoint{
				DNSName:    host,
				RecordTTL:  ttl,
				RecordType: "A",
				Targets:    finalTargets.GetIPs(),
				Labels: externaldns.Labels{
					"strategy": d.gslb.Spec.Strategy.Type,
				},
			}
			for k, v := range d.getLabels(d.gslb, finalTargets) {
				dnsRecord.Labels[k] = v
			}
			gslbHosts = append(gslbHosts, dnsRecord)
		}
	}
	dnsEndpointSpec := externaldnsApi.DNSEndpointSpec{
		Endpoints: gslbHosts,
	}

	dnsEndpoint := &externaldnsApi.DNSEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name:        d.gslb.Name,
			Namespace:   d.gslb.Namespace,
			Annotations: map[string]string{"k8gb.absa.oss/dnstype": "local"},
			Labels:      map[string]string{"k8gb.absa.oss/dnstype": "local"},
		},
		Spec: dnsEndpointSpec,
	}

	return dnsEndpoint, nil
}

func (d *ApplicationDNSEndpoint) GetExternalTargets(host string) (targets Targets) {
	targets = NewTargets()
	gt, err := geotags.GeoTag(d.config).GetExternalClusterNSNamesByHostname(host)
	if err != nil {
		d.logger.
			Err(err).
			Str("host", host).
			Msg("Failed to get external cluster ns names")
		return targets
	}
	for tag, cluster := range gt {
		// Use edgeDNSServer for resolution of NS names and fallback to local nameservers
		d.logger.Info().
			Str("cluster", cluster).
			Msg("Adding external Gslb targets from cluster")
		glueA, err := d.queryService.Query(cluster, d.config.ParentZoneDNSServers)
		if err != nil {
			d.logger.Warn().
				Str("fqdn", cluster+".").
				Str("nameservers", d.config.ParentZoneDNSServers.String()).
				Err(err).
				Msg("can't lookup GlueA record")
			continue
		}
		d.logger.Info().
			Str("nameserver", cluster).
			Str("edgeDNSServers", d.config.ParentZoneDNSServers.String()).
			Interface("glueARecord", glueA.Answer).
			Msg("Resolved glue A record for NS")
		glueARecords := d.queryService.ExtractARecords(glueA)
		var hostToUse string
		if len(glueARecords) > 0 {
			hostToUse = glueARecords[0]
		} else {
			hostToUse = cluster
		}
		nameServersToUse := getNSCombinations(d.config.ParentZoneDNSServers, hostToUse)
		lHost := fmt.Sprintf("localtargets-%s", host)
		a, err := d.queryService.Query(lHost, nameServersToUse)
		if err != nil {
			d.logger.Warn().
				Str("fqdn", lHost+".").
				Str("nameservers", nameServersToUse.String()).
				Err(err).
				Msg("can't resolve FQDN using nameservers")
			continue
		}
		clusterTargets := d.queryService.ExtractARecords(a)
		if len(clusterTargets) > 0 {
			targets[tag] = &Target{clusterTargets}
			d.logger.Info().
				Strs("clusterTargets", clusterTargets).
				Str("cluster", cluster).
				Msg("Extend Gslb targets by targets from cluster")
		}
	}
	return targets
}

// getLabels map of where key identifies region and weight, value identifies IP.
func (d *ApplicationDNSEndpoint) getLabels(gslb *k8gbv1beta1.Gslb, targets Targets) (labels map[string]string) {
	labels = make(map[string]string)
	for k, v := range gslb.Spec.Strategy.Weight {
		t, found := targets[k]
		if !found {
			continue
		}
		for i, ip := range t.IPs {
			l := fmt.Sprintf("weight-%s-%v-%v", k, i, v)
			labels[l] = ip
		}
	}
	return labels
}
